// ------------------------------------------------------------------------------------
// REVISION LOG ENTRY
//
// File name  : https://js-extension-afx.googlecode.com/svn/trunk/html5-text-extension.js
// Revision By: (C) Copyright 2014 Hiroyuki Tominaga, All Rights Reserved.
//
// Revised on : 2014-06-21
// Summary    : HTMLInputElement, HTMLTextAreaElement extension implementation.
//
// JQuery is optional. HOWEVER, WHEN USING THE "scrollTo", JQuery IS REQUIRED.
//
// demo: http://jsfiddle.net/motrohi/bAFr6/
//
// ------------------------------------------------------------------------------------
/*
この script は chrome において最適化されています。
IE, firefox などの browser では, 意図した挙動をしない可能性があります。

This script has been optimized in chrome.
There is a possibility of not being able to perform the behavior intended by the designer in the browser IE, such as firefox.
*/

// string utility functions
var string = {
	// count character.
	cc: function (_this, ch, start, last_idx) {
	    var count = 0;
	    last_idx === undefined && (last_idx = _this.length);
	    while (last_idx-- > start) {
	        _this[last_idx] === ch && count++;
	    }
	    return count;
	},
	// remove string range.
	rm: function (_this, start, length) {
	    length === undefined && (length = 1), start < 0 && (start = 0);
	    return _this.substring(0, start) + _this.substring(start + length);
	},
	// format string
	f: function (_this/*...*/) {
	    var arglen = arguments.length;
	    if (arglen === 0)
	        return _this;

	    for (
	        var index = 0; index < arglen;
	    ) {
	        var value = arguments[ index ];
	        if(value && typeof value !== "string")
	            value = value.toString(); // convert to string...

	        var exp = "{" + index++ + "}";
	        if (_this.indexOf(exp) !== -1)
	            _this = _this.split(exp).join(value);
	    }
	    return _this;
	}
};
// if jQuery not defined.
window.$ = window.$ || {};
// 2014-06-19 20:53:56 from jquery ui.
(function($) {
	var kc = $.keyCode;
    !kc && ( $.keyCode = kc = { ENTER: 13, TAB: 9 } );
	for (var alpha = "DYZ", i = 0; i < 3; i++)
		kc[alpha[i]] = alpha[i].charCodeAt(0);
})(window.$);


(function () {

    var _DEBUG = 0;

    var _consume = window.consumeE || function _consume(e, immediate) {
        var method = immediate? "stopImmediatePropagation": "stopPropagation";
        e[method]();
        e.preventDefault();
    };

    function URData(text, start, end) {
        this.text = text,
        this.start = start;
        this.end = end;
    }
    URData.prototype = {
        is: function (ur) {
            if (ur !== undefined) {
                return ur.text === this.text;/* &&
                ur.start === this.start &&
                ur.end === this.end;*/
            }
            return false;
        },
        sync: function(ta) {
            ta.value = this.text;
            ta.selectionStart = this.start;
            ta.selectionEnd = this.end;
        }
    };
    // take snap shot of textarea element.
    function _takeSnap(ta) {
        return new URData(
            ta.value, ta.selectionStart, ta.selectionEnd
        );
    }

    /* UndoRedoManager Provider */
    var CACHE_LIMIT = 100;
    var URMProvider =
    {
        map: {},

        register: function (ta) {
            var uid = new Date().getTime();
            ta._urm_id = uid.toString(10);
            this.map[ta._urm_id] = { undo: [], redo: [] };
        },
        unRegister: function (ta) {
            delete this.map[ta._urm_id];
            delete ta._urm_id;
        },

        getManager: function (ta) {
            return this.map[ta._urm_id];
        },
        // if data nothing it returns undefined.
        getUndo: function (ta) {
            return this.internalGet(ta._urm_id, "undo");
        },
        // if data nothing it returns undefined.
        getRedo: function (ta) {
            return this.internalGet(ta._urm_id, "redo");
        },
        internalGet: function(id, name) {
            var m = this.map[id];
            return m !== undefined? m[name].pop(): null;
        },

        /* max cache default is 50 */
        addUndo: function (ta) {
            _DEBUG && console.log("addUndo, caller=%s", this.addUndo.caller.name);
            this.internalAdd(ta, "undo");
        },
        addRedo: function (ta) {
            _DEBUG && console.log("addRedo, caller=%s", this.addRedo.caller.name);
            this.internalAdd(ta, "redo");
        },
        /*internalAdd: function(ta, mname) {
            var m = this.map[ta._urm_id];
            if (m !== undefined) {
                if (m = m[mname], m.length < CACHE_LIMIT) {
                    var data = _takeSnap(ta);
                    if (this.notifyListeners(data, mname), !m.length) {
                        m.push(data);
                        return;
                    }
                    var prev = m[m.length - 1];
                    !prev.is(data) && m.push(data);
                } else {
                    console.log("reached to cache limit.");
                }
            }
        },*/ // NOTE: 2015-07-19 19:19:02 rewrite
        internalAdd: function(ta, mname) {
            var m = this.map[ta._urm_id];
            if (m !== undefined) {
                if (m = m[mname], m.length < CACHE_LIMIT) {
                    var data = _takeSnap(ta);
                    this.notifyListeners(data, mname);
                    if (!m.length || !m[m.length - 1].is(data)) {
                        m.push(data);
                    }
                } else {
                    console.log("reached to cache limit.");
                }
            }
        },


        /** [example for listener]
         *  function handleUndoRedoEvent(data, type)
         *  {
         *      var pick_left = pick_text_length, pick_right = pick_text_length;
         *      var text = data.text;
         *      var start = data.start;
         *      var end = data.end;
         *
         *      if (start < pick_left)
         *          pick_left = start;
         *      if (end > text.length - pick_right)
         *          pick_right = end;
         *
         *      var l = text.substring(start - pick_left, start);
         *      var r = text.substring(end, end + pick_right);
         *      var select = text.substring(start, end);
         *      $dom("ur_type").innerHTML = type;
         *      $dom("log").innerHTML = (
         *          l.encloseHTML("span", "pick-left") +
         *          select.encloseHTML("span", "pick-select") +
         *          r.encloseHTML("span", "pick-right")
         *      );
         *  }
         */
        _listeners: [],
        addListener: function (l) {
            typeof l === "function" && (this._listeners.push(l));
        },
        notifyListeners: function (data, type) {
            for (var ls = this._listeners, i = ls.length; --i >= 0; ls[i](data, type));
        },
        setCacheLimit: function (limit) {
            limit < 0 && (limit = 100), CACHE_LIMIT = limit;
        },
        /**
         * @param ta  can use selector string, "#element.id", ".element-class"
         *            In that case, jQuery is required.
         */
        clear: function (ta) {
            if (typeof ta === "string") {
                var ch = ta.charAt(0);
                ta = (ch === "#" || ch === ".")? $(ta)[0]: document.getElementById(ta);
            }
            delete this.map[ta._urm_id];
            this.map[ta._urm_id] = { undo: [], redo: [] };
        }
    };

    /**
     * extension export interface.
     */
    window.HTML5TextEx = {
        getURMProvider: function () { return URMProvider; },
        // jQuery を利用した event observe, namespace を使用.
        observeEvents$: function (element, callback, regex, remove, capturephaze) {
            function _obe_(ev) {
                console.log(ev.type);
            }
            !$.isFunction(callback) && (callback = _obe_);
            capturephaze === undefined && (capturephaze = false);
            var method = (remove? "remove": "add") + "EventListener";
            for (var prop in element) {
                try {
                    if (prop.indexOf("on") === 0) {
                        if ( (regex instanceof RegExp) && regex.test(prop) )
                            continue;

                        element[method](prop.substring(2), callback, capturephaze);
                    }
                } catch (e) {
                    console.log(e.message);
                }
            }
        },
        setDebug: function(is) { _DEBUG = is; }
    };
/*
    function _fireTextEvent(sval) {
        var te = document.createEvent('TextEvent');
        te.initTextEvent('textInput', true, true, null, sval);
        this.dispatchEvent(te);
    }
*/
    // insertText(text, index)
    /**
     * @param text  Text to be inserted
     * @param index Index to be inserted
     */
    function insertText(text, index) {
        if ( /text|textarea/i.test(this.type) ) {
            var sval = this.value;
            if (!sval || !index) index = 0;
            this.value = sval.substring(0, index) + text + sval.substring(index);
        }
    }

    // insertTextToCurrentCaret(text)
    /**
     * @param text  Text to be inserted
     */
    function insertTextToCurrentCaret(text) {
        if (this.type === "text" || this.type === "textarea") {
            /*var sval = this.value;
            var index = this.selectionStart;
            this.value = sval.substring(0, index) + text + sval.substring(index);*/
            var start = this.selectionStart, end = this.selectionEnd;
            this.value = _replaceRange(this.value, text, start, start);
            this.selectionStart = start += text.length;
            this.selectionEnd = start;
        }
    }
    // replaceSelection(text)
    /**
     * @param text  Text to be replace.
     */
    function replaceSelection(text) {
        if (this.type === "text" || this.type === "textarea") {
            /*var sval = this.value;
            var start = this.selectionStart;
            var end = this.selectionEnd;
            this.value = sval.substring(0, start) + text + sval.substring(end);*/
            var start = this.selectionStart, end = this.selectionEnd;
            var contents = this.value, resutls;
            this.value = resutls = _replaceRange(contents, text, start, end);
            this.selectionStart = start, this.selectionEnd = end + (resutls.length - contents.length);

        }
    }
    function _replaceRange(text, replacement, start, end) {
        return text.substring(0, start) + replacement + text.substring(end);
    }

    // findSelect(text, start)
    /**
     * @param text  Text to be select.
     * @param start Index to start the search.
     * @param force_forcus for firefox flag, will force the focus.
     */
    function findSelect(text, start, force_forcus) {
        if (this.type === "text" || this.type === "textarea") {
            var sval = this.value;
            if (typeof start !== "number" || start < 0)
                start = 0;
            var x = sval.indexOf(text, start);
            if (x !== -1) {
                this.selectionStart = x;
                this.selectionEnd = x + text.length;
                force_forcus && this.focus();
                _DEBUG && console.log( string.f("text={2}, start{0}, end={1}", x, this.selectionEnd, text) );
            }
        }
    }

    var SCROLL_LEFT_ADJUST = 2;
    var FACTOR = 0.75;
    //
    // JQuery require.
    //
    /**
     * NOTE: MUST set "line-height" css property.
     * @param text_offset  Offset of the text to scroll.
     */
    function scrollTo(text_offset) {
        var jqo = $(this);
        var sval = this.value || this.innerText; {
            var l_h = parseInt( jqo.css("line-height") );
            var line_count = string.cc(sval, "\n", 0, text_offset);
            _DEBUG && console.log( string.f("l_h={0}, line_count={1}", l_h, line_count) );
            this.scrollTop = line_count * l_h;
        }
        /* scroll left に関しては, ある程度妥協が必要. */
        var cw = parseInt( jqo.css("font-size") ) / 2; // 2 で割るとちょうど良い(英数字
        var prev_lf = sval.lastIndexOf("\n", text_offset);
        prev_lf < 0 && (prev_lf = 0);
        var ival = text_offset - prev_lf;
        ival = cw * ( ival - (ival >= 2? SCROLL_LEFT_ADJUST: 0) );
        _DEBUG && console.log(
            string.f("start={3}, prev_lf={2}, ival={0}, leftFactor={1}", ival, jqo.innerWidth() * FACTOR, prev_lf, text_offset)
        );
        ival < jqo.innerWidth() * FACTOR && (ival = 0);
//        (ival + cw * SCROLL_LEFT_ADJUST) <= jqo.innerWidth() && (ival = 0);
        this.scrollLeft = ival;
    }


    /**
     * HTMLTextAreaElement smart edit implementation.
     *
     * Usage: HTMLTextAreaElement.installSmart(tab_string, event_namespace)
     *
     *    document.getElementById("textarea.id").installSmart("  ", event_namespace);
     *
     * @param tab_string      like "    ", "  ", "    ".
     * @param event_namespace Can be used when include the jQuery.
     *    you are instructing the program to use the event bind the jQuery.
     *    Please use the namespace such as "html5-text", will be event name like "keydown.html5-text paste.html5-text cut.html5-text".
     *    will register the event as, "keydown paste cut" event if you do not specify anything.
     *
     * binding:
     *
     *  [TAB]       : indentRight
     *  [shift+TAB] : indentLeft
     *
     *  [ctrl+D]       : deleteLineFromCaret
     *  [ctrl+shift+D] : copyLineFromCaret
     *
     *  [ENTER]     : smart indent.(add whitespace to head of new line.(whitespace is preview line copy
     *
     */
    /* array */
    function clarificationOfTheRange(str, start, last_start, no_content) {
        // NOTE: 2014/01/24 この section は潜在的に bug がある?
        for (; start > 0;) { /* search line start */
            if(str.charAt(--start) === '\n') {
                start++; break;
            }
        }
        var limit = str.length;
        for (; last_start < limit; last_start++) { /* search line end */
            if(str.charAt(last_start) === '\n') {
                break;
            }
        }
        return [
        	!no_content? str.substring(start, last_start): void 0,
        	start, last_start
        ];
    }
    /* array */
    function prepareIndent(str, start, last) {
        calculate_valid_selection: {
            var result = clarificationOfTheRange(str, start, last);
            _DEBUG && console.log("prepareIndent parse, start=%s, end=%d", result[1], result[2]);
            return result;
        }
    }

    /* boolean */
    // ^([^\n][\s]+)?(.+)$
    // ^([ \t]+)?(.+)$
    function indentRight() {
        var st, ed, sb/* string buffer */;
        calculate_valid_selection: {
            var result = prepareIndent(this.value, this.selectionStart, this.selectionEnd);
            sb = result[0], st = result[1], ed = result[2];
        }
        var tab = this.tab_string;
        Insert_Tabs: {
            /*var x0, x1;
            if (sb.charAt(0) !== '\n') {
                sb = tab + sb, x0 = tab.length;
            } else x0 = 0;
            do {
                x0 = sb.indexOf('\n', x0);
                if (x0 === -1) break;
                x1 = sb.indexOf('\n', x0 + 1);
                if (x1 === -1) {
                    (x0 < sb.length - 1) && (
                        sb = sb.substring(0, x0 + 1) + tab + sb.substring(x0 + 1)
                    );
                    break;
                }
                if (x1 - x0 === 1) {
                    x0 += 1; continue;
                }
                sb = sb.substring(0, x0 + 1) + tab + sb.substring(x0 + 1);
                x0 = x1 + 1;
            } while (true);*/
            var regex = /^.+$/gm;
            sb = sb.replace(regex, function ($0) {
            	return tab + $0;
            });
        }
        URMProvider.addUndo(this), _DEBUG && console.log("indentRight, start=%s, end=%s", st, ed);
        var check_lf = this.value.indexOf("\n", st);
        (check_lf === ed || check_lf === -1) ? this.insertTextToCurrentCaret(tab): (
            this.selectionStart = st, this.selectionEnd = ed,
            this.replaceSelection(sb)
        );
        return true;
    }
    /* boolean */
    function indentLeft() {
        var st, ed, sb/* string buffer */, sval = this.value;
        calculate_valid_selection: {
            var result = prepareIndent(sval, this.selectionStart, this.selectionEnd);
            sb = result[0], st = result[1], ed = result[2];
        }
        var tab = this.tab_string;
        remove_Tabs: {
            /*var x0 = 0, x1;
            if (sb.indexOf(tab, 0) === 0)
                sb = sb.remove(0, tab.length);
            do {
                x0 = sb.indexOf('\n', x0);
                if (x0 === -1) break;
                if ( (++x0) < sb.length && sb.indexOf(tab, x0) === x0 )
                	sb = sb.remove(x0, tab.length);
            } while (true);*/
            //var regex = /^([ \t]+)?.+$/gm;
            var regex = /^[ \t]+/gm;
            sb = sb.replace(regex, function ($0) {
                return $0.indexOf(tab, 0) >= 0? $0.substring(tab.length): $0;
            });
        }
		URMProvider.addUndo(this), _DEBUG && console.log("indentLeft, st=%s, ed=%s", st, ed);
        if ( sval.indexOf("\n", st) === ed ) {
            var start = this.selectionStart;//, end = this.selectionEnd;
            var x = sval.lastIndexOf(tab, start);
            if (x >= st) {
                   //console.log("indentLeft, selectionStart=%s, selectionEnd=%s, lastIndex", start, end, x);
                this.value = sval.substring(0, x) + sval.substring(x + tab.length);
                this.selectionStart = start - tab.length, this.selectionEnd = start - tab.length;
            }
        } else {
            this.selectionStart = st, this.selectionEnd = ed;
            this.replaceSelection(sb);
        }
        return true;
    }

	/**
	 * will copy the line at the caret position of the current textarea.
	 */
	function copyLineFromCaret() {
        var st, ed, sb/* string buffer */, sval = this.value;
        calculate_valid_selection: {
            var result = clarificationOfTheRange(sval, this.selectionStart, this.selectionStart);
            sb = result[0], st = result[1], ed = result[2];
        }
		URMProvider.addUndo(this);
		this.value = sval.substring(0, ed) + "\n" + sb + sval.substring(ed++);
		this.selectionStart = ed, this.selectionEnd = ed;
		this._s_cdl && window.$ && this.scrollTo(ed);
		return true;
	}

	/**
	 * will remove the line at the caret position of the current textarea.
	 */
	function deleteLineFromCaret() {
        var st, ed, sval = this.value;
        calculate_valid_selection: {
            var result = clarificationOfTheRange(sval, this.selectionStart, this.selectionStart, 1);
            st = result[1], ed = result[2] + 1;
        }
		if (st === 0 && ed === 0) return false;
		URMProvider.addUndo(this);
		this.value = string.rm(sval, st, ed - st), this.selectionStart = st, this.selectionEnd = st;
		this._s_cdl && window.$ && this.scrollTo(st);
		return true;
	}

    /* boolean */
    function smartIndent() {
        var x, str = this.value;
        if(str.length === 0)
            return false;

        var position = this.selectionStart, st = 0;
        for (x = position - 1; x >= 0; x--) { /* search line start */
            if(str.charAt(x) === '\n') {
                st = x + 1; break;
            }
        }
        if (st < 0)
            st = 0;
        var limit = position;//str.length;
        var sb = "\n";
        for (x = st; x < limit; x++) { /* search line start */
            var ch = str.charAt(x);
            if(ch === ' ' || ch === '\t') sb += ch;
            else break;
        }
        // Undo, redo is available when use the TextEvent.
        // However, undo fine grained, it is not possible to redo.
        /* this._fireTextEvent(sb); */
        URMProvider.addUndo(this);
        this.insertTextToCurrentCaret(sb);
        this.selectionStart = position += sb.length;
        this.selectionEnd = position;
        return true; /* consume event */
    }


    // exclude "input", "input" event cannot obtain keyCode.
    var BIND_EVENTS = "keydown paste cut";
    function _bindEvents(ta, method) {
        if (ta.type === "textarea") {
            BIND_EVENTS.split(" ").forEach(function(e, i, arry) {
                ta[method](e, _smartEdit, false);
            });
        }
    }
    function _add_namespace(tokens, ns, delim) {
        var ns_events = [];
        if (delim === undefined) delim = " ";
        tokens.split(delim).forEach(function(e, i, arry) {
            ns_events.push(e + "." + ns);
        });
        return ns_events.join(delim);
    }


    /**
     * @param tab_string      insert tab string, "    ", "  ", "    " etc.
     *                        default is "    ".
     * @param event_namespace Can be used when include the jQuery.
     * @param scroll_cpline   scroll caret on copy/delete line if need.
     */
    function installSmart(tab_string, event_namespace, scroll_cpline) {
    	tab_string === undefined && (tab_string = "    ");
    	if ( !/^([ ]+)$|^([\t]+)$/.test(tab_string) ) {
    		console.log("can specify only tab character or space.");
    		return;
    	}
        this.tab_string = tab_string;
        this._s_cdl = scroll_cpline;
        if (event_namespace !== undefined && event_namespace.length > 0) {
            var events_ns = _add_namespace(BIND_EVENTS, event_namespace);
            $(this).on(events_ns, _smartEdit);
            this.event_namespace = events_ns;
        } else {
            _bindEvents(this, "addEventListener");
        }
        URMProvider.register(this);
    }
    function unInstallSmart() {
        if (this.event_namespace) {
            $(this).unbind(this.event_namespace, _smartEdit);
            delete this.event_namespace;
        }
        else {
            _bindEvents(this, "removeEventListener");
        }
        delete this.tab_string;
        delete this._s_cdl;
        URMProvider.unRegister(this);
    }

    // keydown > keypress > input > keyup (chrome 35
    // textarea.installSmart(); textarea.unInstallSmart();
    function _smartEdit(e)
    {
        var _this = e.target, kcode = e.keyCode;
        var $kc = $.keyCode;
        if (e.type === "keydown") {
            _DEBUG && console.log("_smartEdit, enter [%s], kcode=%s", e.type, kcode);
            if (e.ctrlKey) {
                switch (kcode) {
                    case $kc.Z:
                    case $kc.Y: {
                        var data, getmethod, addmethod;
                        (kcode === $kc.Z) ? (
                            getmethod = "getUndo", addmethod = "addRedo"
                        ): (
                            getmethod = "getRedo", addmethod = "addUndo"
                        )
                        data = URMProvider[getmethod](_this); _DEBUG && console.log(data);
                        data && (
                            URMProvider[addmethod](_this), data.sync(_this)
                        );
                        _DEBUG && console.log("_smartEdit, undo or redo, manager=%o", URMProvider.map);
                        _consume(e);
                        break;
                    }
                    case $kc.D: {
                        (e.shiftKey)? _this.copyLineFromCaret(): _this.deleteLineFromCaret();
                        _consume(e);
                        break;
                    }
                }    // switch (kcode)
            } else { // if (e.ctrlKey)
                URMProvider.addUndo(_this);
                switch (kcode) {
                    case $kc.TAB: {
                        ( e.shiftKey? _this.indentLeft(): _this.indentRight() ) && _consume(e);
                        break;
                    }
                    case $kc.ENTER: {
                        /* MUST CONSUME EVENT. Otherwise, edits can not be applied. */
                        _this.smartIndent() && _consume(e);
                        break;
                    }
                }
            }
            return;
        }
        /*1 && console.log("_smartEdit, enter [%s], event=%o, manager=%o", e.type, e, URMProvider.map);
        if (e.type === "input") {
            URMProvider.addUndo(_this);
        }*/
        else {
        	(e.type === "paste" || e.type === "cut") && URMProvider.addUndo(_this);
        }
    }

	/* install functions */
    var clazz = HTMLTextAreaElement;
    clazz.prototype.installSmart = installSmart;
    clazz.prototype.unInstallSmart = unInstallSmart;

    clazz.prototype.indentLeft = indentLeft;
    clazz.prototype.indentRight = indentRight;

    clazz.prototype.copyLineFromCaret = copyLineFromCaret;
    clazz.prototype.deleteLineFromCaret = deleteLineFromCaret;

    clazz.prototype.smartIndent = smartIndent;

    for (var i = 0; i < arguments.length; i++) {
        clazz = arguments[i];
        clazz.prototype.scrollTo = scrollTo;
        clazz.prototype.insertText = insertText;
        clazz.prototype.findSelect = findSelect;
        clazz.prototype.replaceSelection = replaceSelection;
        clazz.prototype.insertTextToCurrentCaret = insertTextToCurrentCaret;
        /* private function*/
        //clazz.prototype._fireTextEvent = _fireTextEvent;
    }

})(HTMLInputElement, HTMLTextAreaElement);


